package com.nervenets.general.utils;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.nervenets.general.exception.LogicException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Description: 使用SpringEL+自定义注解CsvColumn实现的Csv内容初始化通用工具
 * 功能点：
 * 1.当数据为空时，默认输出头信息
 * 2.处理因为单元格内容包含特殊字符导致的CSV显示格式异常的问题
 * 3.基于缓存提高CSV文件格式及头信息获取性能
 * 4.基于SpringEl实现自定义输出格式
 * 5.基于ResponseEntity屏蔽底层Servlet Api
 * 6.
 * 使用：
 * 在需要导出的DTO或PO的field加上@CsvColumn，该field就会写入csv
 * 本类为了方便，引入了lang3包的StringUtils和Guava的Cache 如果项目本身没有相关依赖或者对于外部依赖有严格的控制
 * 请自行重构。其中Cache是基于concurrentHashMap实现（并发安全）
 * 重构时需注意缓存初始化时的并发安全问题,可使用ConcurrentHashMap也可以使用锁...
 */
@Slf4j
public class CsvUtil {
    /**
     * CSV文件列分隔符
     */
    private static final String CSV_COLUMN_SEPARATOR = ",";

    /**
     * CSV文件数据出现和分隔符相同时的替换字符（也可以转译）
     */
    private static final String CSV_COLUMN_SEPARATOR_REPLACE = ".";

    /**
     * CSV文件换行符
     */
    private static final String CSV_RN = "\r\n";
    /**
     * CSV文件名前缀（按照需求自行重构）
     */
    private static final String FILE_PREFIX = "";


    /**
     * 被@CsvColumn注解的字段缓存（如果存在大量（上百个）的csv下载，可以考虑缓存增加失效时间）
     * key：Class+group
     * value：Map<String, CsvColumn>
     * key：filedName  value：CsvColumn
     */
    private static final Cache<String, Map<String, CsvColumn>> annotationMapCache = CacheBuilder.newBuilder().build();
    /**
     * csv文件标题行数据缓存
     * key：Map<String, CsvColumn>对象的hashCode Hex
     * value：Csv行数据
     */
    private static final Cache<String, String> csvHeadLineCache = CacheBuilder.newBuilder().build();

    public static <T> void sendDataStream(HttpServletResponse response, List<T> dataList, String fileName, Class<T> dataClass) throws IOException {
        sendDataStream(response, dataList, fileName, null, dataClass, null);
    }

    public static <T> void sendDataStream(HttpServletResponse response, List<T> dataList, String fileName, Class<T> dataClass, Class<?>... childClasses) throws IOException {
        sendDataStream(response, dataList, fileName, null, dataClass, childClasses);
    }

    public static <T> void sendDataStream(HttpServletResponse response, List<T> dataList, String fileName, String group, Class<T> dataClass, Class<?>... childClass) throws IOException {
        final Map<String, CsvColumn> filedAnnotationMap = getFiledAnnotationMap(dataClass, group, childClass);
        StringBuilder builder = new StringBuilder();
        //1.拼接头信息
        builder.append(getCsvHeaderLine(filedAnnotationMap, dataList, dataClass, childClass));
        builder.append(CSV_RN);
        //2.拼接数据列
        if (!CollectionUtils.isEmpty(dataList)) {
            dataList.forEach(obj -> builder.append(getCsvOneLine(filedAnnotationMap, obj, dataClass, childClass)));
        }
        //3.将数据写入response流中
        writeData(response, builder.toString(), fileName);
    }

    public static String appendLine(String source, String... dataList) {
        StringBuilder builder = new StringBuilder(source);
        for (String str : dataList) {
            String dataColumn = Optional.ofNullable(str).orElse("");
            builder.append("\"");
            builder.append(symbolTranslation(dataColumn));
            builder.append("\"");
            builder.append(CSV_COLUMN_SEPARATOR);
        }
        builder.append(CSV_RN);
        return builder.toString();
    }

    /**
     * 先从缓存中获取，如果获取不到，初始化数据并放入本地缓存
     * 缓存过程线程安全
     *
     * @param dataClass 数据对应class
     * @param group     分组编码
     * @return getIfPresent排序后的Map
     */
    private static Map<String, CsvColumn> getFiledAnnotationMap(Class<?> dataClass, String group, Class<?>... childClasses) {
        return Optional.ofNullable(annotationMapCache.getIfPresent(getAnnotationMapKey(dataClass, group)))
                .orElseGet(() -> initFiledAnnotationMap(dataClass, group, childClasses));
    }

    private static Map<String, Field> getFieldsMap(Class<?> dataClass, Class<?>... childClasses) {
        // 遍历所有属性
        Field[] fields = initFields(dataClass, childClasses);
        Map<String, Field> fieldMap = new HashMap<>();
        Arrays.asList(fields).forEach(field -> {
            fieldMap.put(field.getName(), field);
        });
        return fieldMap;
    }

    private static Field[] initFields(Class<?> dataClass, Class<?>... childClasses) {
        // 遍历所有属性
        Field[] fields = null;
        List<Field> fieldList = new ArrayList<>();
        List<Field> childFieldList = new ArrayList<>();
        // 遍历父类
        while (dataClass != null) {
            fieldList.addAll(Arrays.asList(dataClass.getDeclaredFields()));
            dataClass = dataClass.getSuperclass();
        }
        // 遍历属性对象 注意这里的属性子对象不会再次遍历获取父类和自己的属性子对象，后期可根据业务可做深度递归
        if (null != childClasses) {
            for (Class<?> childClass : childClasses) {
                fieldList.forEach(field -> {
                    if (field.getType().equals(childClass)) {
                        childFieldList.addAll(Arrays.asList(childClass.getDeclaredFields()));
                    }
                });
            }
        }

        //合并
        fieldList.addAll(childFieldList);

        if (!CollectionUtils.isEmpty(fieldList)) {
            fields = fieldList.toArray(new Field[fieldList.size()]);
        }
        return fields;
    }

    private static Map<String, CsvColumn> initFiledAnnotationMap(Class<?> dataClass, String group, Class<?>... childClasses) {
        Map<String, CsvColumn> columnMap = new LinkedHashMap<>();
        //1.查找带有@CsvColumn注解的field，并装入CsvColumnMap
        Field[] fields = initFields(dataClass, childClasses);
        Arrays.asList(fields).forEach(field -> {
            CsvColumn annotation = field.getDeclaredAnnotation(CsvColumn.class);
            if (annotation != null) {
                field.setAccessible(true);
                //剔除分组过滤的字段
                if ((StringUtils.isNotEmpty(group) && StringUtils.isNotEmpty(annotation.doGroup()) && !annotation.doGroup().equals(group)) ||
                        (StringUtils.isNotEmpty(annotation.unDoGroup()) && annotation.unDoGroup().equals(group))) {
                    return;
                }
                columnMap.put(field.getName(), annotation);
            }
        });
        //2.根据FileCsvColumn的weight属性对CsvColumnMap进行排序.
        final Map<String, CsvColumn> filedAnnotationMap = sortByValue(columnMap);
        //3.加入缓存
        annotationMapCache.put(getAnnotationMapKey(dataClass, group), filedAnnotationMap);
        return filedAnnotationMap;
    }

    /**
     * 拼接CVS表格一行数据
     *
     * @param filedAnnotationMap
     * @return
     */
    private static <T> String getCsvOneLine(Map<String, CsvColumn> filedAnnotationMap, T lineData, Class<?> dataClass, Class<?>... childClasses) {
        StringBuilder lineStrBuilder = new StringBuilder();
        //1循环data，一个obj代表一行
        filedAnnotationMap.forEach((key, csvColumn) -> {
            //2循环filedAnnotationMap，一个Entity代表一列的数据
            try {
                Field field = getFieldsMap(dataClass, childClasses).get(key);
                field.setAccessible(true);
                Object obj = field.get(lineData);
                if (obj != null && childClasses != null && Arrays.asList(childClasses).contains(obj.getClass())) {
                    for (Class<?> childClass : childClasses) {
                        if (obj.getClass().equals(childClass)) {
                            Arrays.asList(csvColumn.childProperties()).forEach(childProperty -> {
                                try {
                                    Field f = obj.getClass().getDeclaredField(childProperty);
                                    f.setAccessible(true);
                                    String dataColumn = Optional.ofNullable(f.get(obj)).orElse("").toString();
                                    lineStrBuilder.append("\"");
                                    lineStrBuilder.append(symbolTranslation(dataColumn));
                                    lineStrBuilder.append("\"");
                                    lineStrBuilder.append(CSV_COLUMN_SEPARATOR);
                                } catch (IllegalAccessException | NoSuchFieldException e) {
                                    e.printStackTrace();
                                    log.error(e.getMessage());
                                }
                            });
                        }
                    }
                } else {
                    if (csvColumn.dynamicTitles()) {
                        try {
                            List<CsvDynamicColumn> dynamicColumns = (List<CsvDynamicColumn>) field.get(lineData);
                            dynamicColumns.forEach(csvDynamicColumn -> {
                                String dataColumn = Optional.ofNullable(csvDynamicColumn.getContent()).orElse("");
                                lineStrBuilder.append("\"");
                                lineStrBuilder.append(symbolTranslation(dataColumn));
                                lineStrBuilder.append("\"");
                                lineStrBuilder.append(CSV_COLUMN_SEPARATOR);
                            });
                        } catch (Exception e) {
                            throw new LogicException("CSV动态头部配置错误", e);
                        }
                    } else {
                        String dataColumn = Optional.ofNullable(obj).orElse("").toString();
                        //3解析SpringEL表达式，处理自定义的输出格式需求
                        if (StringUtils.isNotEmpty(csvColumn.springEL())) {
                            dataColumn = getSpringELValue(csvColumn.springEL(), lineData);
                        }
                        //4转译处理(放在el解析后，防止el解析逻辑出现幺蛾子（EL转译后出现CSV逻辑符号）)
                        lineStrBuilder.append("\"");
                        lineStrBuilder.append(symbolTranslation(dataColumn));
                        lineStrBuilder.append("\"");
                        lineStrBuilder.append(CSV_COLUMN_SEPARATOR);
                    }
                }
            } catch (IllegalAccessException e) {
                log.error("CsvUtil根据反射操作属性异常，异常信息{0}", e);
            }
        });
        return lineStrBuilder.append(CSV_RN).toString();
    }

    /**
     * 获取
     *
     * @param filedAnnotationMap
     * @return String
     */
    private static <T> String getCsvHeaderLine(Map<String, CsvColumn> filedAnnotationMap, List<T> dataList, Class<?> dataClass, Class<?>... childClasses) {
        return StringUtils.join(Optional.ofNullable(csvHeadLineCache.getIfPresent(Integer.toHexString(filedAnnotationMap.hashCode())))
                .orElseGet(() -> initCsvHeaderLine(filedAnnotationMap, dataList, dataClass, childClasses)), CSV_COLUMN_SEPARATOR);
    }

    /**
     * 获取annotationMapCache的key
     *
     * @param dataClass
     * @param group
     * @return
     */
    private static String getAnnotationMapKey(Class<?> dataClass, String group) {
        return dataClass.getName().concat(Optional.ofNullable(group).orElse(""));
    }

    private static <T> String initCsvHeaderLine(Map<String, CsvColumn> filedAnnotationMap, List<T> dataList, Class<?> dataClass, Class<?>... childClasses) {
        List<String> csvHeaderLines = new ArrayList<>();
        filedAnnotationMap.forEach((filed, csvColumn) -> {
            if (csvColumn.dynamicTitles()) {
                if (!dataList.isEmpty()) {
                    Field field = getFieldsMap(dataClass, childClasses).get(filed);
                    field.setAccessible(true);
                    try {
                        List<CsvDynamicColumn> dynamicColumns = (List<CsvDynamicColumn>) field.get(dataList.get(0));
                        csvHeaderLines.addAll(dynamicColumns.stream().map(CsvDynamicColumn::getTitle).collect(Collectors.toList()));
                    } catch (Exception e) {
                        throw new LogicException("CSV动态头部配置错误", e);
                    }
                }
            } else {
                if (csvColumn.titles().length != 0) {
                    csvHeaderLines.addAll(Arrays.asList(csvColumn.titles()));
                }

                if (StringUtils.isNotEmpty(csvColumn.title())) {
                    csvHeaderLines.add(csvColumn.title());
                }
            }
        });
        final String csvHeaderLineStr = StringUtils.join(csvHeaderLines, CSV_COLUMN_SEPARATOR);
        csvHeadLineCache.put(Integer.toHexString(filedAnnotationMap.hashCode()), csvHeaderLineStr);

        return csvHeaderLineStr;
    }

    /**
     * 根据csv的内容 使用HttpServletResponse 发送
     *
     * @param data csv内容
     * @return
     */
    public static void writeData(HttpServletResponse response, String data, String fileName) throws IOException {
        //此行为了标示文件解析的格式，不加在Excel上会乱码，wps好像没事
        data = "\ufeff".concat(data);
//        return ResponseEntity.ok()
//                .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8"))
//                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;fileName=".concat(getRealCsvFileName(fileName) + ".csv"))
//                .body(new InputStreamResource(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))));
        response.setContentType("text/csv");
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;fileName=".concat(getRealCsvFileName(fileName) + ".csv"));
        response.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
    }

    private static String getSpringELValue(String springEL, Object sourceObj) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(springEL);
        return exp.getValue(sourceObj) + "";
    }

    /**
     * 根据FileColumn中的weight属性为Map<String, CsvColumn> map排序
     *
     * @param map 需要排序的map
     * @return 排序后的CsvColumn Map
     */
    private static Map<String, CsvColumn> sortByValue(Map<String, CsvColumn> map) {
        Map<String, CsvColumn> result = new LinkedHashMap<>();
        map.entrySet().stream()
                .sorted(Comparator.comparing(entry -> entry.getValue().weight()))
                .forEach(e -> result.put(e.getKey(), e.getValue()));
        return result;
    }

    private static String getRealCsvFileName(String fileName) throws UnsupportedEncodingException {
        if (com.nervenets.general.utils.StringUtils.isBlank(fileName)) fileName = "无名";
        return FileUtil.setFileDownloadHeader(Objects.requireNonNull(HttpTools.getHttpRequest()), fileName);
    }

    /**
     * 特殊符号转译，防止内容中包含CSV的逻辑符号
     * 针对英文的",",与CSV的列标示冲突，统一更换为中文的"，"
     * 针对"\r\n",与CSV的行标示冲突，统一更换为""
     * 也可以自己定制
     *
     * @param dataColumn 单元格内容
     * @return 转译后的单元格内容
     */
    private static String symbolTranslation(String dataColumn) {
        return dataColumn.replaceAll(CSV_RN, " ").replaceAll(CSV_COLUMN_SEPARATOR, "，");
    }

}
